JDBCSQL注入
CAMELLIA!!! note 目录
SQL注入
SQL注入是一种网络攻击,利用应用程序未正确处理用户输入数据的漏洞,通过在输入字段插入恶意的SQL代码来执行数据库操作。
这种攻击可能导致数据泄露、数据篡改或者完全控制数据库。
一、SQL注入现象
假设有一个登录页面,用户需要输入用户名和密码。应用程序接收到用户输入后,构建了如下的SQL查询语句
1
| SELECT * FROM users WHERE username='输入的用户名' AND password='输入的密码';
|
攻击者可以在用户名和密码字段中插入恶意的SQL代码,比如输入 ' OR '1'='1
,那么构建出来的查询语句就变成了:
1
| SELECT * FROM users WHERE username='' OR '1'='1' AND password='' OR '1'='1';
|
这样,无论输入的用户名和密码是什么,条件 '1'='1'
都为真,因此查询将返回所有用户的记录,从而绕过了身份验证,攻击者就可以获取所有用户的信息。
示例代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
| package com.jdbc.userlogin;
import java.sql.*; import java.util.HashMap; import java.util.Map; import java.util.ResourceBundle; import java.util.Scanner;
public class UserLogin { public static void main(String[] args) { Map<String,String> userLoginInfo= initUI(); boolean loginSuccess=login(userLoginInfo); System.out.println(loginSuccess?"登入成功":"登入失败"); }
private static boolean login(Map<String, String> userLoginInfo) { boolean loginSuccess=false; ResourceBundle rb = ResourceBundle.getBundle("userlogin"); Connection conn = null; Statement stmt = null; ResultSet rs = null; String driver = rb.getString("driver"); String url = rb.getString("url"); String username = rb.getString("username"); String password = rb.getString("password"); try{ Class.forName(driver); conn= DriverManager.getConnection(url, username, password); stmt = conn.createStatement(); String sql="select * from t_user where username='"+userLoginInfo.get("username")+"' and password='"+userLoginInfo.get("password")+"'"; rs=stmt.executeQuery(sql); if(rs.next())loginSuccess=true; }catch (SQLException e){ e.printStackTrace(); }catch (ClassNotFoundException e){ e.printStackTrace(); } finally { try{if(rs!=null)rs.close();}catch(SQLException e){e.printStackTrace();} try{if(stmt!=null)stmt.close();}catch(SQLException e){e.printStackTrace();} try{if(conn!=null)conn.close();}catch(SQLException e){e.printStackTrace();} } return loginSuccess; }
private static Map<String, String> initUI() { Scanner scanner = new Scanner(System.in); System.out.println("用户名:"); String username = scanner.nextLine(); System.out.println("密码:"); String password = scanner.nextLine(); Map<String, String> userLoginInfo = new HashMap<String, String>(); userLoginInfo.put("username", username); userLoginInfo.put("password", password); return userLoginInfo; } }
|
二、解决SQL注入问题
- 只要用户提供的信息不参与SQL语句的编译过程,问题就解决了。
即使用户提供的信息中含有SQL语句的关键字,但是没有参与编译,不起作用。
而为了让用户信息不参与编译,可以使用``java.sql.PreparedStatement`。
PreparedStatement
接口继承了``java.sql.Statement`。
PreparedStatement
是属于预编译的数据库操作对象。
PreparedStatement
的原理:预先对SQL语句的框架进行编译,然后再给SQL语句传值。
2.1 `PreparedStatement实现
使用PreparedStatement
的代码步骤顺序有所不同。
注册驱动
获取连接
1
| conn= DriverManager.getConnection(url, username, password);
|
==获取预编译的数据库操作对象==
1 2 3 4 5 6 7
| String sql="select * from t_user where username = ? and password = ?";
pstmt=conn.prepareStatement(sql);
pstmt.setString(1,username); pstmt.setString(2,password);
|
- SQL语句的位置有所调动,需在
prepareStatement
创建之前。
- SQL语句框架中,?表示一个占位符。一个占位符将来接受一个值,即使有关键字也作为普通字符处理。
- 注意:占位符不能用单引号括起来。
执行SQL
1
| rs=pstmt.executeQuery();
|
在不需要再传sql了,因为在创建prepareStatement就已经传递过了。
处理结果集
1 2
| if(rs.next())loginSuccess=true;
|
释放资源
1 2 3
| if(rs!=null)try {rs.close();} catch (SQLException e) {e.printStackTrace();} if(pstmt!=null)try {pstmt.close();} catch (SQLException e) {e.printStackTrace();} if(conn!=null)try {conn.close();} catch (SQLException e) {e.printStackTrace();}
|
2.2 示例代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
| package com.jdbc.userlogin;
import java.sql.*; import java.util.HashMap; import java.util.Map; import java.util.ResourceBundle; import java.util.Scanner;
public class SQLInjectionSolution { public static void main(String[] args) { Map<String,String> userLoginInfo= initUI(); boolean loginSuccess=login(userLoginInfo); System.out.println(loginSuccess?"登入成功":"登入失败"); }
private static boolean login(Map<String, String> userLoginInfo) { boolean loginSuccess=false; ResourceBundle rb = ResourceBundle.getBundle("userlogin"); Connection conn = null; PreparedStatement pstmt = null; ResultSet rs = null; String driver = rb.getString("driver"); String url = rb.getString("url"); String username = rb.getString("username"); String password = rb.getString("password"); try{ Class.forName(driver); conn= DriverManager.getConnection(url, username, password); String sql="select * from t_user where username = ? and password = ?"; pstmt=conn.prepareStatement(sql); pstmt.setString(1,username); pstmt.setString(2,password); rs=pstmt.executeQuery(); if(rs.next())loginSuccess=true; }catch (SQLException e){ e.printStackTrace(); }catch (ClassNotFoundException e){ e.printStackTrace(); } finally { try{if(rs!=null)rs.close();}catch(SQLException e){e.printStackTrace();} try{if(pstmt!=null)pstmt.close();}catch(SQLException e){e.printStackTrace();} try{if(conn!=null)conn.close();}catch(SQLException e){e.printStackTrace();} } return loginSuccess; }
private static Map<String, String> initUI() { Scanner scanner = new Scanner(System.in); System.out.println("用户名:"); String username = scanner.nextLine(); System.out.println("密码:"); String password = scanner.nextLine(); Map<String, String> userLoginInfo = new HashMap<String, String>(); userLoginInfo.put("username", username); userLoginInfo.put("password", password); return userLoginInfo; } }
|
三、prepareStatement和Statement对比
3.1 效率方面
PreparedStatement
预编译和缓存:
PreparedStatement
预编译SQL语句,并将其缓存起来。在需要多次执行相同的SQL语句时,数据库可以重复使用预编译的查询计划,而无需重新编译。这节省了编译时间,提高了性能。
Statement
每次执行都要编译:
- 每次执行
Statement
时,SQL语句都会被重新解析、编译和优化,这会增加数据库的开销,尤其是当相同的SQL语句被多次执行时。
所以SQL语句只执行一次,它两效率差不多。
3.2 类型安全检测
1. PreparedStatement
- 参数化查询:
PreparedStatement
允许使用占位符(?
)来代表参数,并使用特定的方法(如 setString()
、setInt()
等)来设置这些参数的值。
- 自动类型检查:由于每个参数都通过特定的方法设置,因此 JDBC 驱动程序会自动进行类型检查,确保传递的值符合SQL查询的要求。
示例代码:
1 2 3 4 5 6 7 8 9
| String sql = "INSERT INTO t_user (username, password, age) VALUES (?, ?, ?)"; PreparedStatement pstmt = connection.prepareStatement(sql);
pstmt.setString(1, username); pstmt.setString(2, password); pstmt.setInt(3, age);
pstmt.executeUpdate();
|
在上面的代码中:
- 第一个参数被设置为字符串类型(
setString
),第二个参数也被设置为字符串类型,第三个参数被设置为整数类型(setInt
)。
- JDBC 驱动程序会确保这些参数的类型与SQL语句中的预期类型一致。如果类型不匹配,会在编译时或运行时抛出
SQLException
。
2. Statement
- 直接拼接SQL字符串:
Statement
通过拼接字符串来构建SQL语句。
- 缺乏类型检查:由于SQL语句是动态构建的,JDBC驱动程序无法在构建SQL语句时进行类型检查,这可能导致SQL注入风险和运行时错误。
示例代码:
1 2 3 4 5 6 7
| String username = "username"; String password = "password"; int age = 30;
String sql = "INSERT INTO t_user (username, password, age) VALUES ('" + username + "', '" + password + "', " + age + ")"; Statement stmt = connection.createStatement(); stmt.executeUpdate(sql);
|
在上面的代码中:
- 所有的SQL查询部分都是通过字符串拼接完成的。
- 缺乏类型检查,容易导致SQL注入问题,也容易因为字符串拼接错误导致语法错误。
总结:
PreparedStatement
没有sql注入问题。
PreparedStatemen
t有预编译和缓存,效率更高。
PreparedStatement
支持类型安全检测。
3.3 何时用Statement?
虽然Statement有很多缺点,会导致SQL注入。但是有的程序功能必须使用Statement,需要进行SQL语句的拼接,利用SQL注入传入SQL关键字。
例如、商城的按价格排序。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| package com.jdbc.sort;
import java.sql.*; import java.util.ResourceBundle; import java.util.Scanner;
public class JDBCSort { public static void main(String[] args) { Scanner sc = new Scanner(System.in); System.out.println("请输入desc或者asc:"); String keyWords = sc.nextLine();
ResourceBundle rb = ResourceBundle.getBundle("jdbc"); String driver = rb.getString("driver"); String url = rb.getString("url"); String username = rb.getString("username"); String password = rb.getString("password"); Connection conn = null; Statement stmt = null; ResultSet rs = null; try { Class.forName(driver); conn= DriverManager.getConnection(url,username,password); stmt = conn.createStatement(); String sql="select id from user order by id "+keyWords; rs=stmt.executeQuery(sql); while(rs.next()){ System.out.println("id:"+rs.getInt(1)); }
} catch (ClassNotFoundException e) { throw new RuntimeException(e); }catch (SQLException e){ throw new RuntimeException(e); } finally { if(rs!=null)try {rs.close();} catch (SQLException e) {e.printStackTrace();} if(stmt!=null)try {stmt.close();} catch (SQLException e) {e.printStackTrace();} if(conn!=null)try {conn.close();} catch (SQLException e) {e.printStackTrace();} }
} }
|